FEAT: Collapse solutions by default under solution_follow_exercise - #86
FEAT: Collapse solutions by default under solution_follow_exercise#86mmcky wants to merge 6 commits into
Conversation
Note on the red CI: pre-existing, unrelated to this branchThe
raised from
Because of this, the gated test on this branch was moved off the The underlying breakage is left untouched here — it needs either a pydata-sphinx-theme/sphinx pin or a theme change in that test root, and that is a separate call for the maintainer. Verified locally on this branch: 127 tests pass on Sphinx 8.2.3, and |
|
The CI breakage described above now has a fix in #88, which is green across all 13 checks. Once that merges I'll rebase this branch onto it, at which point this PR's CI should go fully green too. The pre-existing bugs noted in #85 are now filed as #89 (gated directives crash when the start directive is suppressed), #90 (gated merge mangles |
|
Verified the rebase will be clean: merging #88 into this branch locally and running the full suite under the exact failing CI combination (Python 3.13, Sphinx 6.2.1, pydata-sphinx-theme 0.20.0) gives 132 passed, 1 skipped — the skip being the sphinx-togglebutton test in an env that lacks it. The only failures on this PR's current CI are the four |
Adds a `solution_collapsed` boolean configuration option that renders every solution directive folded by default, so readers opt in to seeing the answer. This addresses reader feedback on books that use `exercise_style = "solution_follow_exercise"`, where an inline solution sitting directly under its exercise is hard to look away from. The option adds the `dropdown` class to solution nodes, which is the class sphinx-togglebutton already consumes (its default selector is `.toggle, .admonition.dropdown`). It is equivalent to writing `:class: dropdown` on every solution, and is applied in `SolutionDirective.run()`, which `SolutionStartDirective` inherits, so gated `solution-start` / `solution-end` pairs are covered by the same code path. Details: - Registered with the "env" rebuild trigger, matching `hide_solutions` and `exercise_style`, because the class is injected at read time and baked into the pickled doctree. - Directive-level `:class:` values are preserved, and an explicit `:class: dropdown` is not duplicated. - `:class: toggle-shown` keeps an individual solution expanded while the rest of the project is collapsed. - Non-HTML builders are unaffected: the LaTeX branch of `visit_solution_node` never reads `node["classes"]`, so solutions render inline as before. - An HTML build with the option enabled but no extension providing the `dropdown` class emits a warning, rather than silently rendering every solution expanded. - Default is `False`, so existing projects and all committed regression fixtures are unchanged. Adds `sphinx_togglebutton` to the `testing` extra so the no-warning path can be covered; the test skips when it is unavailable. Refs #84 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The `gateddirective` test root sets `html_theme = "sphinx_book_theme"`, which is currently broken on Sphinx 6 and 7 in CI: a newer pydata-sphinx-theme calls `_get_toctree_ancestors`, which does not exist in those Sphinx versions, so every test using that root fails with a ThemeError. Five pre-existing `test_gateddirective.py` tests fail the same way; that breakage is unrelated to this branch and is left alone here. Adds a self-contained gated fixture to the `mybook` test root, which uses alabaster and is unaffected, and points the gated collapsed test at it. The new fixture is also much lighter than the `gateddirective` one, which executes matplotlib code cells. Verified the fixture genuinely exercises the merge path rather than falling back: the rendered solution carries `class="solution dropdown admonition"`, the title resolves to "Solution to Exercise 7 (A gated example)", the intervening content is merged into the admonition, and no solution-end marker survives. Also adds a default-off counterpart so the gated path is covered in both directions. Refs #85 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Three fixes from an adversarial review of the initial implementation. 1. Drop `sphinx_design` from `TOGGLE_EXTENSIONS`. It does not provide the `dropdown` class: its dropdown is a directive emitting `.sd-dropdown`, and its stylesheet ships no rule for a bare `dropdown` class (verified — the compiled CSS contains only `.sd-dropdown`). Listing it suppressed the warning in exactly the case the warning exists for, and because Jupyter Book loads sphinx-design by default, a Jupyter Book project without sphinx-togglebutton would have silently rendered every solution expanded with no diagnostic. 2. Give the warning `type="exercise"` / `subtype="solution_collapsed"`. It was untyped, so it could not be suppressed and made `-W` builds fail outright for projects that supply their own `.admonition.dropdown` CSS instead of loading the extension — a setup the check's own docstring anticipates. `suppress_warnings = ["exercise.solution_collapsed"]` now silences it while still applying the class. 3. Document that collapsing hides content by zeroing its height rather than removing it, so outputs that measure themselves at load time (plotly, bokeh, ipywidgets, altair) render at zero size inside a collapsed solution. This matters for exactly the executable-book audience the option targets. Static images including matplotlib are unaffected; `:class: toggle-shown` is the per-directive escape hatch. Also hardens the test fixtures: the two new toctree entries move to the end of `test-mybook/index.rst` and the gated fixture's exercise becomes `:nonumber:`, so neither can shift the global exercise numbers baked into ~40 committed regression fixtures. A comment records the constraint for future entries. Adds a test that the warning is suppressible while the class is still applied. Verified: 127 tests pass; the new tests pass on Sphinx 6.2.1, 7.4.7 and 8.2.3; docs build adds no new warnings. Refs #85 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
These six .sphinx9 baselines were generated accidentally while reproducing an unrelated CI failure in a scratch environment that had been upgraded past the supported Sphinx range. pytest-regressions writes a new baseline when it finds no file for the running version's suffix, so they were created as a side effect rather than deliberately. They do not belong in the tree: the project supports sphinx>=6.1,<9, so there is no CI job that would ever read them, and they record output produced with a theme/Sphinx combination the suite does not target. Refs #85 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Folds collapsing into what the `solution_follow_exercise` exercise style means, rather than leaving it as an unrelated switch authors must find. That style places each solution directly beneath its exercise, which is exactly the layout the reader feedback in #84 was about: an adjacent, fully visible solution is too tempting to look at. Treating the fold as part of the style means the option authors already reach for does the right thing by default. `solution_collapsed` becomes tri-state to make this expressible: unset (None) follow exercise_style - collapsed under solution_follow_exercise True always collapse False never collapse The `None` default is load-bearing, not stylistic. Sphinx cannot distinguish "unset" from "explicitly set to False" through the public config API, so with the previous `False` default there would have been no way to switch the style's implied collapsing back off. Resolution lives in `utils.solutions_are_collapsed(config)` rather than being written back into the config at `config-inited`. That keeps the raw tri-state readable, so the warning can tell whether collapsing was asked for or implied, avoids mutating config other extensions may read, and needs no rebuild-trigger handling of its own since `exercise_style` is already registered "env". The missing-togglebutton warning now adapts. An author who opted in explicitly is told to add the extension; an author who only set `exercise_style` and never asked for collapsing is told how to switch it off as well: exercise_style='solution_follow_exercise' collapses solutions by default, but 'sphinx_togglebutton' is not loaded, so they will render expanded. Add 'sphinx_togglebutton' to your extensions, or set solution_collapsed = False to keep solutions expanded. This is a behaviour change for projects on `solution_follow_exercise`, so it is documented as one: a "Changed" entry in the CHANGELOG leading with how to restore the old rendering, and new v1.3.0 release notes opening with the change and the one-line opt-out. The syntax guide gains a value table for the tri-state, and the Solution Title Styling section now lists collapsing among what the style does. Jupyter Book ships sphinx_togglebutton in its default extension list, so Jupyter Book projects need no change. Six new tests cover the interaction: the style collapsing by default, explicit False overriding it, explicit True agreeing with it without duplicating the class, both warning texts, and silence when the implied collapse is switched off. Verified: 133 tests pass; the collapsed and exercise_style suites pass together on Sphinx 6.2.1, 7.4.7 and 8.2.3; docs build adds no new warnings. Refs #84, #85 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The gated collapsed tests use the RST `.. solution-start::` form in the
`mybook` test root. This adds the MyST ```{solution-start}``` counterpart in
the `gateddirective` root, so the directive-option parsing both parsers feed
into is exercised from each side.
That root was unusable while its `sphinx_book_theme` setting was breaking CI on
Sphinx 6 and 7, which is why the earlier gated test was written against
`mybook` instead. #88 has since switched it to alabaster, so it is available
again.
The `mybook` fixture stays as the primary gated test: it is self-contained, it
sits alongside the rest of the collapsed suite so confoverrides stay
consistent, and it avoids executing the matplotlib code cells the
`gateddirective` root builds.
Verified: 134 tests pass.
Refs #85
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
f01fd7b to
1a78590
Compare
Adds a
solution_collapsedconfiguration option that renders solutions folded by default, and makes collapsing part of what thesolution_follow_exerciseexercise style means.Closes #85. Addresses the first of the two options proposed in #84; the appendix option is tracked separately in #87.
Motivation
Reader feedback on book-dp1, which uses
exercise_style = "solution_follow_exercise":The layout that is best for authoring (solution next to exercise) is not the one that is best for reading. This lets authors keep writing solutions inline and get a folded presentation at publish time, without touching the source.
Projects using
exercise_style = "solution_follow_exercise"will now render solutions collapsed. That style is precisely the layout the feedback is about, so the fold is treated as part of the style rather than as a separate switch authors have to discover.The opt-out is one line:
Nothing changes for projects that do not set
exercise_style, and no source files need editing either way. Documented as aChanged ⚠️entry in the CHANGELOG and in new v1.3.0 release notes, both leading with how to restore the previous rendering.The tri-state, and why it is necessary
solution_collapsedsolution_follow_exercise, expanded otherwiseTrueFalseThe
Nonedefault is load-bearing rather than stylistic. Sphinx cannot distinguish "unset" from "explicitly set toFalse" through the public config API, so with a plainFalsedefault there would be no way to switch the style's implied collapsing back off — the opt-out above would be impossible to express.Resolution lives in
utils.solutions_are_collapsed(config)rather than being written back into the config atconfig-inited. That keeps the raw tri-state readable (so the warning can tell whether collapsing was asked for or implied), avoids mutating config other extensions may read, and needs no rebuild-trigger handling of its own, sinceexercise_styleis already registered"env".How it works
Collapsing adds the
dropdownclass, which sphinx-togglebutton already consumes — its defaulttogglebutton_selectoris.toggle, .admonition.dropdown, and the solution renders as<div class="solution dropdown admonition" id="...">.The injection point is a single block in
SolutionDirective.run().SolutionStartDirectivesubclasses it and ends inreturn super().run(), so gated{solution-start}/{solution-end}pairs are covered by the same code path. The class survivesMergeGatedSolutions, which rebuilds the class list on merge, because that rebuild is a per-element substring replace anddropdowncontains nosolution-startsubstring. That is the code path the #82 class-list bug lived in, so it has a dedicated test.Behaviour matrix
exercise_styleclass="solution admonition"exercise_style = "solution_follow_exercise"solution_collapsed = Falsesolution_collapsed = True{solution-start}/{solution-end}:class: my-classdropdown:class: dropdown:class: toggle-shownsuppress_warnings = ["exercise.solution_collapsed"]LaTeX is provably unaffected: the
LaTeXTranslatorbranch ofvisit_solution_nodeemits only\phantomsection\label{...}plus a constant\begin{sphinxadmonition}{note}, and never readsnode["classes"].The warning adapts to how collapsing was enabled
An author who opted in explicitly is told to add the extension. An author who only set
exercise_style, and never asked for collapsing, is also told how to switch it off:Jupyter Book ships
sphinx_togglebuttonin its default extension list, so Jupyter Book projects will not see this. The check runs atbuilder-inited, is gated onapp.builder.format == "html"so it adds no noise to LaTeX builds, and is typedexercise.solution_collapsedso it can be suppressed rather than locking projects out of-Wbuilds.sphinx-design deliberately does not count as a provider. Its dropdown is a directive emitting
.sd-dropdown, and its stylesheet has no rule for a baredropdownclass. Since Jupyter Book loads sphinx-design by default, treating it as a provider would suppress the warning for exactly the projects that need it.One caveat is documented rather than fixed: collapsing hides content by zeroing its height rather than removing it, so outputs that measure themselves at load time (plotly, bokeh, ipywidgets, altair) render at zero size inside a collapsed solution. Static images including matplotlib are fine, and
:class: toggle-shownis the per-directive escape hatch.Tests
Eighteen tests in
tests/test_solution_collapsed.pycovering each row of the matrix, both warning texts, the gated path from both RST and MyST source, and the tri-state interaction withexercise_style. Two fixtures in themybooktest root exercise the explicit-:class: dropdownde-duplication case and the gated path; both are appended at the end of the toctree and the gated one's exercise is:nonumber:, so neither can shift the global exercise numbers baked into the committed regression fixtures.sphinx_togglebuttonis added to thetestingextra so the no-warning path can be covered; that test skips when it is unavailable.Verified locally:
exercise_stylesuites pass together on Sphinx 6.2.1, 7.4.7 and 8.2.3pre-commit(ruff + ruff-format at the pinned v0.1.4) passesDocs
A Collapse All Solutions section in
docs/source/syntax.mdwith the tri-state value table, bothconf.pyand Jupyter Book_config.ymlforms, the LaTeX fallback note, the interactive-output caveat and the:class: toggle-shownescape hatch. The Solution Title Styling section now lists collapsing among what the style does, with the opt-out. Newdocs/source/releases/v1.3.0.md, added to the release-notes toctree.🤖 Generated with Claude Code